ciftiTools is an R package for working with CIFTI-2 format brain imaging data. It supports the following CIFTI file types: ".dscalar.nii", ".dtseries.nii", and ".dlabel.nii". It also supports the GIFTI surface geometry file, ".surf.gii". Reading, writing, and resampling CIFTI files is made possible using the Connectome Workbench. The Workbench must be installed to use ciftiTools. Visualizing the CIFTI files is made possible using the rgl R package and integrated support of surface GIFTI files.

To get started, we load the ciftiTools package and indicate where to find the Connectome Workbench folder:

library(knitr)
library(rgl)

out_dir <- "output"

opts_chunk$set(out.width="75%")
knit_hooks$set(webgl = rgl::hook_webgl)

# Sometimes the first RGL window does not render properly.
rgl.open(); rgl.close()
#devtools::install_github("mandymejia/ciftiTools")
library(ciftiTools)
# Replace '/path/to/workbench' with the actual path to the Connectome Workbench folder on your computer.
ciftiTools.setOption('wb_path', '../../workbench')

# Cleaner vignette document
ciftiTools.setOption("suppress_msgs", TRUE)

In this vignette, we will use example data included in the ciftiTools package. The files are originally from NITRC:

cifti_fnames <- list(
  dtseries = system.file("extdata", "Conte69.MyelinAndCorrThickness.32k_fs_LR.dtseries.nii", package = "ciftiTools"),
  dscalar = system.file("extdata", "Conte69.MyelinAndCorrThickness.32k_fs_LR.dscalar.nii", package = "ciftiTools"),
  dlabel = system.file("extdata", "Conte69.parcellations_VGD11b.32k_fs_LR.dlabel.nii", package = "ciftiTools"),
  dscalar_ones = system.file("extdata", "ones.dscalar.nii", package = "ciftiTools")
)
# cifti_fnames <- list(
#   dtseries = file.path("../inst/extdata", "Conte69.MyelinAndCorrThickness.32k_fs_LR.dtseries.nii"),
#   dscalar = file.path("../inst/extdata", "Conte69.MyelinAndCorrThickness.32k_fs_LR.dscalar.nii"),
#   dlabel = file.path("../inst/extdata", "Conte69.parcellations_VGD11b.32k_fs_LR.dlabel.nii"),
#   dscalar_ones = file.path("../inst/extdata","ones.dscalar.nii")
# )

surfL_fname = system.file("extdata", "Conte69.L.inflated.32k_fs_LR.surf.gii", package = "ciftiTools")
surfR_fname = system.file("extdata", "Conte69.R.inflated.32k_fs_LR.surf.gii", package = "ciftiTools")

# surfL_fname = file.path("../inst/extdata", "Conte69.L.inflated.32k_fs_LR.surf.gii")
# surfR_fname = file.path("../inst/extdata", "Conte69.R.inflated.32k_fs_LR.surf.gii")

Reading a CIFTI file with a surface, and viewing it

CIFTI files organize the gray matter of the brain into “grayordinates”: vertices representing the left and right cortical surfaces, and voxels representing the subcortical gray matter structures and the cerebellum. A CIFTI file consists of two parts: (1) a NIFTI XML header which contains all the metadata including medial wall locations, subcortical structure labels, and the subcortical volumetric mask; and (2) a matrix representing all the grayordinate data. These components are read in together with read_cifti:

cii <- read_cifti(cifti_fnames$dtseries)
cii
## Brain Structures: left cortex, right cortex  
##  left cortex: 30424 surface vertices, 2 measurements.
##  right cortex: 30527 surface vertices, 2 measurements.
cii <- read_cifti(cifti_fnames$dscalar)
cii
## Brain Structures: left cortex, right cortex  
##  left cortex: 30424 surface vertices, 2 measurements.
##  right cortex: 30527 surface vertices, 2 measurements.

By default, read_cifti only reads in the left and right cortex data. The subcortical data can be included by using the argument brainstructures="all". Other brainstructure combinations can be specified too, e.g. brainstructures=c("left", "subcortical"). The full set of choices for brainstructures is any combination of "left", "right" and "subcortical", or "all" for all three.

The resulting object produced by read_cifti is a "xifti" with components data (the grayordinate data matrix, separated by brainstructure), meta (metadata, most of which is from the NIFTI XML header), and surf (surface geometry). The last component distinguishes a "xifti" from a CIFTI: the left and right cortical surface geometries are not included in CIFTI files, so they must be read from separate surface GIFTI files (ending in surf.gii). The surface must be compatible: the number of vertices must be the same, and each vertex in the CIFTI data must correspond to the vertex location in the corresponding GIFTI surface file. In this way, a "xifti" represents a combination of a CIFTI file with compatible GIFTI files for the cortical mesh.

We can add surfaces like so:

cii <- add_surf(cii, surfL=surfL_fname, surfR=surfR_fname)
cii
## Brain Structures: left cortex, right cortex, left surface, right surface  
##  left cortex: 30424 surface vertices, 2 measurements.
##      left surface model is present.
##  right cortex: 30527 surface vertices, 2 measurements.
##      right surface model is present.

Alternatively, we could have provided the surfaces at the outset of reading the CIFTI file:

cii <- read_cifti(cifti_fnames$dscalar, surfL_fname=surfL_fname, surfR_fname=surfR_fname)
cii
## Brain Structures: left cortex, right cortex, left surface, right surface  
##  left cortex: 30424 surface vertices, 2 measurements.
##      left surface model is present.
##  right cortex: 30527 surface vertices, 2 measurements.
##      right surface model is present.

Let’s take a look! view_xifti_surface(cii) displays the cortical data on the surface mesh in an interactive Open GL window. Several default color palettes are available through the color_mode argument: "sequential" (default), "qualitative" and "diverging". The "qualitative" color mode is primarily intended for dlabel CIFTI files. The colors argument can be used to choose other palettes. The idx argument can be used to visualize different columns or time points in the data. Finaly, using the argument mode=image the Open GL window can be saved to a static image. To minimize the size of this document, we will use the image method except for one plot in the “Resampling” section.

knitr::include_graphics(view_xifti_surface(
  cii, zlim=c(1,2), mode='image', fname=file.path(out_dir, "xifti_0.png")
))
## Registered S3 method overwritten by 'fields':
##   method       from      
##   plot.surface ciftiTools
First time point; sequential palette

First time point; sequential palette

knitr::include_graphics(view_xifti_surface(
  cii, idx=2, zlim=c(1,5), color_mode='diverging', mode='image', 
  fname=file.path(out_dir, "xifti_1.png")
))
Second time point; diverging palette

Second time point; diverging palette

cii <- read_cifti(cifti_fnames$dlabel, surfL=surfL_fname, surfR=surfR_fname)
knitr::include_graphics(view_xifti_surface(
  cii, mode='image', fname=file.path(out_dir, "xifti_2.png")
))
.dlabel file; first label; palette from label metadata

.dlabel file; first label; palette from label metadata

view_xifti_volume(cii) displays the subcortical data in slices. To view interactively in a web browser, set use_papaya=TRUE. By default, a series of slices is displayed overlaid on the MNI template. The orientation, numbers of slices, index and value range can be adjusted.

# cifti_fnames$dscalar_ones is the only file with subcortical data
cii <- read_cifti(cifti_fnames$dscalar_ones, brainstructures="subcortical")
view_xifti_volume(cii)
## Values to be plotted range from 1 to 1.
Subcortical data (all ones)

Subcortical data (all ones)

# For information only, since papaya viewer cannot be opened during knitting
view_xifti_volume(cii, use_papaya = TRUE)

The "xifti" “plot” method (plot(cii)) will display the cortical data if possible, and the subcortical data otherwise.

More about the "xifti"

Medial wall of the cortical data

Medial wall vertices are not included in the cortex_left and cortex_right components of data. A data matrix for the left cortex which includes the medial wall vertices can be obtained with unmask_cortex(cii$data$cortex_left, cii$meta$cortex$medial_wall_mask$left) (and similarly for the right cortex). If the medial walls were not masked out in the input CIFTI file, the medial_wall_mask entries will be NULL.

Vectorization of the subcortical data

The subcortical data is stored in vectorized form. To recover the subcortical volume, use unmask_vol(cii$data$subcort, cii$meta$subcort$mask, fill=NA) for the data and unmask_vol(cii$meta$subcort$labels, cii$meta$subcort$mask, fill=0) for the labels.

Intent code

cii$meta$cifti$intent indicates the NIFTI intent, which corresponds to a unique CIFTI file type. For example, "dtseries.nii" files have an intent of 3006.

Support for surface geometry without data

A "surface" can be read in using make_surf. They can be viewed with view_surf or, equivalently, their plot method. Here is the left hemisphere surface:

knitr::include_graphics(view_surf(
  surfL_fname, mode="image", fname=file.path(out_dir, "surf_1.png")
))
Left hemisphere surface

Left hemisphere surface

We can additionally render the vertices and edges. Below is the right hemisphere surface. (It has been resampled so the edges and vertices are visible; see the below section on resampling.)

small_surf <- resample_surf(make_surf(surfR_fname), 4000)
# Use ciftiTools:::plot.surface instead of plot because plot.surface is an S3 method from fields package (used to create legend in RGL) too
knitr::include_graphics(ciftiTools:::plot.surface(
  small_surf,
  mode="image", fname=file.path(out_dir, "surf_2.png"),
  # hemisphere="right", # able to be deduced from metadata
  edge_color="black", vertex_size=5
))
Right hemisphere surface (resampled) with mesh

Right hemisphere surface (resampled) with mesh

rm(small_surf)

A "xifti" can contain surface geometry without the corresponding data; to make it, use as.xifti(surfL=make_surf(surfL_fname)).

Creating a new "xifti" and writing it

We can make a "xifti" from data using as.xifti. For example, let’s make a "xifti" from the mean image (over time) of the "dtseries.nii" file. (Note that the dtseries used in this example does not truly contain fMRI timeseries data, but we use it for illustration.)

cii <- read_cifti(cifti_fnames$dtseries)
cii_new <- as.xifti(
  cortexL = apply(cii$data$cortex_left, 1, mean),
  cortexL_mwall = cii$meta$cortex$medial_wall_mask$left,
  cortexR = apply(cii$data$cortex_right, 1, mean),
  cortexR_mwall = cii$meta$cortex$medial_wall_mask$right
)
is.xifti(cii_new)
## [1] TRUE

We can also include artifical subcortical data using the mask from "ones.dscalar.nii".

cii2 <- read_cifti(cifti_fnames$dscalar_ones, brainstructures="subcortical")
vol <- cii2$data$subcort
vol <- vol - 1 + matrix(rnorm(nrow(vol)*ncol(vol)), nrow=nrow(vol))
cii_new <- as.xifti(
  cortexL = apply(cii$data$cortex_left, 1, mean),
  cortexL_mwall = cii$meta$cortex$medial_wall_mask$left,
  cortexR = apply(cii$data$cortex_right, 1, mean),
  cortexR_mwall = cii$meta$cortex$medial_wall_mask$right,
  subcortVol = vol,
  subcortLabs = cii2$meta$subcort$labels,
  subcortMask = cii2$meta$subcort$mask
)
is.xifti(cii_new)
## [1] TRUE
cii_new
## Brain Structures: left cortex, right cortex, subcortex  
##  left cortex: 30424 surface vertices, 1 measurements.
##  right cortex: 30527 surface vertices, 1 measurements.
##  subcortex: 31870 voxels, 1 measurements.
##      subcortical labels:
## 
##       Cortex-L       Cortex-R    Accumbens-L    Accumbens-R     Amygdala-L 
##              0              0            135            140            315 
##     Amygdala-R     Brain Stem      Caudate-L      Caudate-R   Cerebellum-L 
##            332           3472            728            755           8709 
##   Cerebellum-R Diencephalon-L Diencephalon-R  Hippocampus-L  Hippocampus-R 
##           9144            706            712            764            795 
##     Pallidum-L     Pallidum-R      Putamen-L      Putamen-R     Thalamus-L 
##            297            260           1060           1010           1288 
##     Thalamus-R 
##           1248

To visualize the cortical data of the new xifti object, we can add surface geometry with add_surf, or by providing surfaces with the surfL and surfR arguments to view_xifti_surface. Lastly, we can use the inflated surfaces that come with ciftiTools by default:

#cii_new <- add_surf(cii_new, surfL=surfL_fname, surfR=surfR_fname)
knitr::include_graphics(view_xifti_surface(
  cii_new, mode="image", fname=file.path(out_dir, "xifti_3.png"), 
  title=".dtseries Mean Image", zlim=c(0,4)
))
.dtseries mean image

.dtseries mean image

Here’s the subcortical data in sagittal view:

view_xifti_volume(cii_new, plane="sag")
## Values to be plotted range from -4.32833526926697 to 4.44418273355813.
Subcortical data, sagittal view (with random noise)

Subcortical data, sagittal view (with random noise)

We can also write out a new CIFTI file with write_cifti! Here’s how:

written_cii_fname <- file.path(out_dir, "my_new_cifti.dscalar.nii")
write_cifti(cii_new, written_cii_fname)
## Writing left cortex.
## Writing right cortex.
## Writing subcortical data and labels.
## Creating CIFTI file from separated components.
# Verify that if we read the file back in, the result matches.
# Some metadata is lost or added, but beside that, the data is the same.
cii_new_copy <- read_cifti(written_cii_fname, brainstructures="all")
try(testthat::expect_equal(cii_new$data, cii_new_copy$data))
## Error : cii_new$data not equal to cii_new_copy$data.
## Component "cortex_left": Mean relative difference: 3.584765e-08
## Component "cortex_right": Mean relative difference: 3.548024e-08
## Component "subcort": Mean relative difference: 2.151368e-08

There is only a negligible difference between the original and the written-then-read copy due to rounding.

Resampling

ciftiTools can resample CIFTI files to a lower resolution. Here, we resample the 32k "dtseries.nii" file to 6k vertices. We also provide the surfaces and resample them in conjunction.

resampled_cii_fname <- "my_new_resampled.dtseries.nii"
resampled_surfL_fname <- "my_resampled_surfL.surf.gii"
resampled_surfR_fname <- "my_resampled_surfR.surf.gii"
  
cii_6k <- resample_cifti(
  cifti_fnames$dtseries, resampled_cii_fname,
  resamp_res = 6000,
  surfL_fname, surfR_fname,
  resampled_surfL_fname, resampled_surfR_fname,
  write_dir=out_dir
)
## Separating CIFTI file.
## Time difference of 3.953708 secs
## Resampling CIFTI file.
## Time difference of 6.516555 secs
## Merging components into a CIFTI file... 
## Time difference of 0.179733 secs

The new files can be viewed together with read_cifti. Let’s make this one interactive, since the meshes are now much lower-res! Try clicking and dragging around the plot to rotate, and scrolling to zoom in and out. We’ll also use a blue background color to highlight this interactive figure within the vignette.

view_xifti_surface(
  read_cifti(cifti_fname=cii_6k["cifti"], surfL=cii_6k["surfL"], surfR=cii_6k["surfR"]), 
  zoom=7/10, bg="#d4edf5", cex.title=1.25, 
  title=".dtseries resampled to 6k", zlim=c(0,2)
)
# Note: Call rglwidget() here to display inline in RStudio.
# rglwidget() does not work with htmlpreview, so we used the WebGL hook instead.

You must enable Javascript to view this page properly.

# Close pop-up window
rgl.close()

Resampling can also be performed while reading a file into R.

read_cifti(
  cifti_fnames$dscalar, 
  surfL_fname = surfL_fname, surfR_fname = surfR_fname, 
  resamp_res=6000
)
## Brain Structures: left cortex, right cortex, left surface, right surface  
##  left cortex: 5412 surface vertices, 2 measurements.
##      left surface model is present.
##  right cortex: 5434 surface vertices, 2 measurements.
##      right surface model is present.

Surfaces can also be resampled:

surf <- make_surf(surf=surfL_fname)
resample_surf(surf, resamp_res=6000)
## Vertices:  5762 
## Faces:  11520 
## Hemisphere:  left

The surface GIFTI files can be resampled without a CIFTI:

resampled_surfL_fname <- file.path(out_dir, resampled_surfL_fname)
resample_gifti(
  surfL_fname, resampled_surfL_fname, 
  hemisphere="left", resamp_res=6000
)
make_surf(resampled_surfL_fname)
## Vertices:  5762 
## Faces:  11520 
## Hemisphere:  left

Finally, a CIFTI file can be resampled to match a template. This is not always faster than resampling without a template, but it ensures the files are in register with one another and retains additional metadata.

template_cii_fname <- file.path(out_dir, resampled_cii_fname)
target_cii_fname <- file.path(out_dir, "target.dtseries.nii")

# Since it's the same file, the result is similar, but
# the underlying resampling method may be slightly different.
resample_cifti_from_template(
  original_fname=cifti_fnames$dtseries,
  template_fname=template_cii_fname,
  target_fname=target_cii_fname
)

try(testthat::expect_equal(
  read_cifti(template_cii_fname), read_cifti(target_cii_fname)
))
## Error : read_cifti(template_cii_fname) not equal to read_cifti(target_cii_fname).
## Component "meta": Component "cifti": Component "misc": Component "ParentProvenance": 1 string mismatch
## Component "meta": Component "cifti": Component "misc": Component "Provenance": 1 string mismatch

Other functionality

Separating a CIFTI into GIFTI and NIFTI files

The cortical data can be written to GIFTI files, and the subcortical data can be written to a NIFTI file. The files are automatically named unless a new file name is provided.

# Use default names for everything except left cortex
separated_fnames = separate_cifti(
  cifti_fnames$dscalar_ones, brainstructures="all", 
  cortexL_fname="my_left_cortex.func.gii", write_dir=out_dir
)
separated_fnames
##                                                                            cortexL 
## "C:\\Users\\damon\\Desktop\\ciftiTools\\vignettes\\output/my_left_cortex.func.gii" 
##                                                                         ROIcortexL 
##     "C:\\Users\\damon\\Desktop\\ciftiTools\\vignettes\\output/ones.ROI_L.func.gii" 
##                                                                            cortexR 
##         "C:\\Users\\damon\\Desktop\\ciftiTools\\vignettes\\output/ones.R.func.gii" 
##                                                                         ROIcortexR 
##     "C:\\Users\\damon\\Desktop\\ciftiTools\\vignettes\\output/ones.ROI_R.func.gii" 
##                                                                         subcortVol 
##                "C:\\Users\\damon\\Desktop\\ciftiTools\\vignettes\\output/ones.nii" 
##                                                                        subcortLabs 
##         "C:\\Users\\damon\\Desktop\\ciftiTools\\vignettes\\output/ones.labels.nii" 
##                                                                      ROIsubcortVol 
##            "C:\\Users\\damon\\Desktop\\ciftiTools\\vignettes\\output/ones.ROI.nii"

Separated files can be read back in with the oro.nifti/RNifti and gifti packages, and made into a "xifti" object with as.xifti.

Reading only the CIFTI data, or only the CIFTI metadata

When only the data matrix is needed, use the flat=TRUE argument to save time. Note that all brainstructures in the CIFTI file will be read in, and it will not be possible to determine which rows in the data belong to which brainstructure. It will also not be possible to visualize the data.

cii <- read_cifti(cifti_fnames$dscalar, flat=TRUE)
dim(cii)
## [1] 60951     2

To only read the CIFTI header, use info_cifti.

cii_info <- ciftiTools::info_cifti(cifti_fnames$dlabel)

Verify a CIFTI

Use is.xifti to check if one has been properly formed:

cii <- read_cifti(cifti_fnames$dtseries)
is.xifti(cii)
## [1] TRUE

This can be helpful if it was directly edited:

# Make a mistake and have different numbers of columns for the left and right cortex
cii$data$cortex_left <- cii$data$cortex_left[,1,drop=FALSE]
is.xifti(cii)
## The left cortex, right cortex, and subcortical data (when present) must all have the same number of measurements (columns), but they do not.
## The column counts are: 1, 2
## "data" is invalid.
## [1] FALSE

Smooth a CIFTI

Use smooth_cifti to perform smoothing.

smoothed_cii_fname <- file.path(out_dir, "my_smoothed_cifti.dtseries.nii")
smooth_cifti(
  cifti_fnames$dtseries, smoothed_cii_fname,
  surface_sigma=2, volume_sigma=2,
  surfL_fname=surfL_fname, surfR_fname=surfR_fname,
  subcortical_zeroes_as_NA=TRUE
)
knitr::include_graphics(plot(
  read_cifti(smoothed_cii_fname), 
  surfL=surfL_fname, surfR=surfR_fname, mode="image", 
  title="Smoothed CIFTI", zlim=c(1,2),
  fname=file.path(out_dir, "xifti_4.png")
))
Smoothed CIFTI

Smoothed CIFTI